﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Xaml;
using System.Xml;
using System.Xml.Linq;
using NetFrameworkExtensions.Collections;
using System.Linq.Expressions;
using System.Diagnostics.Contracts;
using System.Collections.ObjectModel;

namespace NetFrameworkExtensions.Reflection
{
  public static class TypeExtensions
  {
    private static readonly object localSync = new object();

    private static IDictionary<string, Type> builtInNameToTypeMapping;
    public static IDictionary<string, Type> BuiltInNameToTypeMapping
    {
      get
      {
        InitializeMappings();
        return builtInNameToTypeMapping;
      }
    }

    private static IDictionary<Type, string> builtInTypeToNameMapping;
    public static IDictionary<Type, string> BuiltInTypeToNameMapping
    {
      get
      {
        InitializeMappings();
        return builtInTypeToNameMapping;
      }
    }

    private static IList<Tup<Assembly, IList<string>>> commonAssembliesAndNamespaces;
    public static IList<Tup<Assembly, IList<string>>> CommonAssembliesAndNamespaces
    {
      get
      {
        InitializeMappings();
        return commonAssembliesAndNamespaces;
      }
    }

    public static bool IsCommonType(this Type type)
    {
      if (type.DeclaringType == null)
        return false;
      var data = CommonAssembliesAndNamespaces.FirstOrDefault(t => type.Assembly == t.Item1);
      if (data != null)
        return data.Item2.Contains(type.Namespace);
      return false;
    }

    public static string GetPlainName(this Type type)
    {
      if (type.DeclaringType == null)
      {
        string name;
        if (BuiltInTypeToNameMapping.TryGetValue(type, out name))
          return name;
        var data = CommonAssembliesAndNamespaces.FirstOrDefault(t => type.Assembly == t.Item1);
        if (data != null && data.Item2.Contains(type.Namespace))
          return type.Name.SubstringBeforeLastOrSame("`");
      }
      return type.FullName.SubstringBeforeLastOrSame("`");
    }

    public static string ToCommonString(this Type type, bool allowNull)
    {
      var result = type.ToCommonString();
      if (allowNull && type.IsValueType)
        result = result + "?";
      return result;
    }

    /// <summary>
    /// Returns a string representation (C#-like) of a type, be it regular, generic, array, by ref, pointer or combinations of it
    /// </summary>
    /// <param name="type"></param>
    /// <returns></returns>
    public static string ToCommonString(this Type type)
    {
      if (type == null) throw new ArgumentNullException("type");
      if (type.IsGenericParameter)
        return type.Name;
      if (type.IsArray) // May not work for jagged arrays
        return type.GetElementType().ToCommonString() + "[" + new string(',', type.GetArrayRank() - 1) + "]";
      if (type.IsByRef)
        return "&" + type.GetElementType().ToCommonString();
      if (type.IsPointer)
        return "*" + type.GetElementType().ToCommonString();
      var sb = new StringBuilder();
      sb.Append(type.GetPlainName());
      if (type.IsGenericType)
        sb.Append(type.GetGenericArguments().Select(t => t.ToCommonString()).ToStringList(", ", opening: "<",
                                                                                          closing: ">", empty: "<>"));
      return sb.ToString();
    }

    private static void InitializeMappings()
    {
      if (builtInNameToTypeMapping == null)
      {
        lock (localSync)
        {
          if (builtInNameToTypeMapping == null)
          {
            Tup<string, Type>[] builtInTypeNames =
              new[]
              {
                Tup.Create("object", typeof(object)),
                Tup.Create("string", typeof(string)),
                Tup.Create("char", typeof(char)),
                Tup.Create("bool", typeof(bool)),
                Tup.Create("float", typeof(float)),
                Tup.Create("double", typeof(double)),
                Tup.Create("decimal", typeof(decimal)),
                Tup.Create("byte", typeof(byte)),
                Tup.Create("sbyte", typeof(sbyte)),
                Tup.Create("short", typeof(short)),
                Tup.Create("ushort", typeof(ushort)),
                Tup.Create("int", typeof(int)),
                Tup.Create("uint", typeof(uint)),
                Tup.Create("long", typeof(long)),
                Tup.Create("ulong", typeof(ulong)),
              };
            var builtIn1 = new Dictionary<string, Type>();
            var builtIn2 = new Dictionary<Type, string>();
            foreach (var t in builtInTypeNames)
            {
              builtIn1.Add(t.Item1, t.Item2);
              builtIn2.Add(t.Item2, t.Item1);
            }

            var common =
              new[]
              {
                Tup.Create(typeof(string).Assembly, (IList<string>)new ReadOnlyList<string>(new[] { "System", "System.Collections", "System.Collections.Generic", "System.IO", })),
                Tup.Create(typeof(Uri).Assembly, (IList<string>)new ReadOnlyList<string>(new[] { "System", })),
                Tup.Create(typeof(Enumerable).Assembly, (IList<string>)new ReadOnlyList<string>(new[] { "System", "System.Linq", })),
                Tup.Create(typeof(XamlWriter).Assembly, (IList<string>)new ReadOnlyList<string>(new[] { "System.Windows.Markup", "System.Xaml", })),
                Tup.Create(typeof(XmlDocument).Assembly, (IList<string>)new ReadOnlyList<string>(new[] { "System.Xml", })),
                Tup.Create(typeof(XDocument).Assembly, (IList<string>)new ReadOnlyList<string>(new[] { "System.Xml.Linq", })),
              };

            commonAssembliesAndNamespaces = new ReadOnlyList<Tup<Assembly, IList<string>>>(common);
            builtInTypeToNameMapping = new ReadonlyDictionary<Type, string>(builtIn2);
            var localVar = new ReadonlyDictionary<string, Type>(builtIn1);
            Thread.MemoryBarrier();
            builtInNameToTypeMapping = localVar;
          }
        }
      }
    }

    public static string GetPropertyName<TObject>(this Expression<Func<TObject, object>> expr)
    {
      Contract.Requires(expr != null);
      Contract.Requires(expr.Body is MemberExpression);
      Contract.Requires(((MemberExpression)expr.Body).Member is PropertyInfo);

      return ((PropertyInfo)((MemberExpression)expr.Body).Member).Name;
    }

    #region [ GetAllCustomAttributes ]

    /// TODO: Do it for members too (Properties, Methods and Events). Take care of inheritance.

    /// <summary>Searches and returns attributes. The inheritance chain is not used to find the attributes.</summary>
    /// <typeparam name="T">The type of attribute to search for.</typeparam>
    /// <param name="type">The type which is searched for the attributes.</param>
    /// <returns>Returns all attributes.</returns>
    public static T[] GetAllCustomAttributes<T>(this Type type) where T : Attribute
    {
      return GetAllCustomAttributesInternal(type, typeof(T), false).Select(arg => (T)arg).ToArray();
    }
    public static object[] GetAllCustomAttributes(this Type type, Type attributeType)
    {
      return GetAllCustomAttributesInternal(type, attributeType, false).ToArray();
    }

    /// <summary>Searches and returns attributes.</summary>
    /// <typeparam name="T">The type of attribute to search for.</typeparam>
    /// <param name="type">The type which is searched for the attributes.</param>
    /// <param name="inherit">Specifies whether to search this member's inheritance chain to find the attributes. Interfaces will be searched, too.</param>
    /// <returns>Returns all attributes.</returns>
    public static T[] GetAllCustomAttributes<T>(this Type type, bool inherit) where T : Attribute
    {
      return GetAllCustomAttributesInternal(type, typeof(T), inherit).Select(arg => (T)arg).ToArray();
    }
    public static object[] GetAllCustomAttributes(this Type type, Type attributeType, bool inherit)
    {
      return GetAllCustomAttributesInternal(type, attributeType, inherit).ToArray();
    }

    /// <summary>Private helper for searching attributes.</summary>
    /// <param name="type">The type which is searched for the attribute.</param>
    /// <param name="attributeType">The type of attribute to search for.</param>
    /// <param name="inherit">Specifies whether to search this member's inheritance chain to find the attribute. Interfaces will be searched, too.</param>
    /// <returns>An array that contains all the custom attributes, or an array with zero elements if no attributes are defined.</returns>
    private static object[] GetAllCustomAttributesInternal(Type type, Type attributeType, bool inherit)
    {
      if (!inherit)
      {
        return type.GetCustomAttributes(attributeType, false);
      }

      var attributeCollection = new Collection<object>();
      var baseType = type;

      do
      {
        baseType.GetCustomAttributes(attributeType, true).Apply(attributeCollection.Add);
        baseType = baseType.BaseType;
      }
      while (baseType != null);

      foreach (var interfaceType in type.GetInterfaces())
      {
        GetAllCustomAttributesInternal(interfaceType, attributeType, true).Apply(attributeCollection.Add);
      }

      var attributeArray = new object[attributeCollection.Count];
      attributeCollection.CopyTo(attributeArray, 0);
      return attributeArray;
    }

    /// <summary>Applies a function to every element of the list.</summary>
    private static void Apply<T>(this IEnumerable<T> enumerable, Action<T> function)
    {
      foreach (var item in enumerable)
      {
        function.Invoke(item);
      }
    }
    #endregion
  }
}
